iT邦幫忙

2023 iThome 鐵人賽

DAY 3
0
Software Development

30 天到底能寫多少 Leetcode系列 第 3

[Day3] 區間求和裡躲不掉的 segment tree

  • 分享至 

  • xImage
  •  

對於區間求和問題,線段樹(segment tree) 可以用來處理區間修改、區間查詢的問題。

像是 731. My Calendar II 這題,每次會標記一個區段,區段內的每個位置最多只能被標記兩次。如果一個區段之內有某個點違反規則,則整個區段的標記會宣告失敗。

這題也可以用前綴和來計算,不過需要不停加總計算,複雜度相對高。

另一個解法就是線段樹了,線段數大致概念是做出一顆二元樹,可以用 ListNode 或者直接開 array 來建造 complete tree(這邊是用 ListNode 來做)。然後在這顆 tree 上紀錄資訊(這個節點底下的範圍、lazy update 的值、目前記錄到的最大 count),並用 lazy update 等技巧稍微優化。

下面努力啃懂後的寫法:

class SegmentTree:
    def __init__(self, u, v):
        self.start = u
        self.end = v
        self.left = None
        self.right = None
        self.max = 0
        self.c = 0

    @property
    def mid(self):
        return (self.start + self.end) // 2

    @property
    def left_tree(self):
        if self.left == None:
            self.left = SegmentTree(self.start, self.mid)
        return self.left
    
    @property
    def right_tree(self):
        if self.right == None:
            self.right = SegmentTree(self.mid+1, self.end)
        return self.right
    
    def lazy_update(self):
        self.left_tree.c += self.c
        self.right_tree.c += self.c
        self.max += self.c
        self.c = 0
    
    def update(self, u, v, x):
        if u <= self.start and v >= self.end:
            self.c += x
            return self.max + self.c
        self.lazy_update()
        if u <= self.mid:
            self.max = max(self.max, self.left_tree.update(u, v, x))
            
        if v > self.mid:
            self.max = max(self.max, self.right_tree.update(u, v, x))
        return self.max
    
    def query(self, u, v):
        if u <= self.start and v >= self.end:
            return self.max + self.c

        self.lazy_update()
        res = 0
        if u <= self.mid:
            res = max(res, self.left_tree.query(u, v))
        if v > self.mid:
            res = max(res, self.right_tree.query(u, v))

        return res

class MyCalendarTwo:
    def __init__(self):
        self.tree = SegmentTree(0, 10**9)
        
    def book(self, start: int, end: int) -> bool:
        if self.tree.query(start, end-1) >= 2:
            return False
        self.tree.update(start, end-1, 1)
        return True

不過如果不是為了學線段樹,其實直接維護兩個 list 的寫法。

class MyCalendarTwo:
    def __init__(self):
        self.once = []
        self.double = []

    def book(self, start: int, end: int) -> bool:
        for s, e in self.double:
            if s < end and start < e:
                return False
        
        for s, e in self.once:
            if s < end and start < e:
                self.double.append([max(s, start), min(e, end)])
        
        self.once.append([start, end])
        return True

https://ithelp.ithome.com.tw/upload/images/20230918/2012966284rEy07bHX.png

這張圖是我在 trace code 時畫的,假設這顆線段樹的範圍只有 0-25,然後要標記的線段分別是 [10,15] 和 [7,12](通常都取頭不取尾)。

第一次標記時,query 會把 tree 從原本只有 root 一路切到底,然後 update 會更新最底層 c 和上層 max 的值。

第二次標記時,query 時會進行 lazy_update,把第一次標記的 c 往下層更新。update 則一樣搜尋到最底層,記錄最底層的 c 和上層更新後的 max 值返回。

希望這張圖能稍微有助於理解整個線段樹的運作模式吧。



  • Current count: 5

上一篇
[Day2] 知道了 prefix 順便也了解一下差分吧
下一篇
[Day4] binary indexed tree 簡介
系列文
30 天到底能寫多少 Leetcode21
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言